package firbi.base;

import static firbi.base.util.Constants.Seattle;
import static firbi.base.util.Constants.sROUTE_KEY;
import static firbi.base.util.Constants.sROUTE_LAT_KEY;
import static firbi.base.util.Constants.sROUTE_LON_KEY;
import static firbi.base.util.Constants.sSTOP_KEY;
import static firbi.base.util.Constants.sLOCATION_BUFFER;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.SimpleAdapter;
import android.widget.TabHost;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.TabHost.TabSpec;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;
import com.google.android.maps.MyLocationOverlay;
import com.google.android.maps.Overlay;

import firbi.base.com.BusRoute;
import firbi.base.com.BusStop;
import firbi.base.display.BusStopOverlay;
import firbi.base.display.BusStopOverlayItem;
import firbi.base.util.BusStopComparator;
import firbi.base.util.FirbiHelper;

/** This activity handles user search requests. It can be displayed in either map or list view. 
 *  The main purpose of this activity is to take in user input, check it, then pass
 *  it off to RouteView or StopView to continue the query.
 *  It can also display/list nearby bus stops. If there is no GPS signal , then the map will 
 *  display stops from its center while the list view will display nothing. 
 *  
 * @author Tracie
 *
 */
public class Search extends MapActivity implements LocationListener{
	private boolean THREAD_FLAG;
	
	private static final int sDEFAULT_RADIUS = 230;   //For finding bus stops near a location
	private static final float sMIN_GPS_ACC = 50;
	private static final int sMIN_GPS_TIME = 180000;  //3 minutes 
	
	/* Codes for signaling how to handle certain input */
	protected static final int INPUT_ROUTE = 0;
	protected static final int INPUT_ADDRESS = 1;
	protected static final int INPUT_BUS_STOP = 2;
	protected static final int NO_BUS_STOP = 3;
	protected static final int INPUT_BAD_BUS_STOP = 4;
	protected static final int INPUT_BAD_ROUTE = 5;
	protected static final int NO_GPS = 6;
	protected static final int INPUT_NOTHING = 7;
	
	/* Layout variables */
	private RelativeLayout mMapLayout;
	private LinearLayout mListLayout;
	private ListView mList;
	private MapView mMap;
	
	/* Map information */
	protected boolean mIsMapView;
	protected boolean mOverlaysAreDisplayed;
	protected boolean mValidLocation;
	private LocationManager mLocManager;
	
	/* View items */
	private MapController mMapController;
	private AutoCompleteTextView mSearchText;
	private ImageButton mSearchButton;
	private ImageButton mGPSButton;
	private TextView mListAddress;
	private TextView mListEmpty;
	
	/* for constant map display */
	private GeoPoint mLastTouchCenter;   
	private GeoPoint mLastQueryCenter;   
	
	/* Related to map/overlay displays */
	private GeoPoint mLastLocation; 
	private GeoPoint mLastCenterPoint;
	private MyLocationOverlay mMyLocation;
	private List<Overlay> mMapOverlays;
	private BusStop[] mCurrentBusStops;
	
	/* Dialogs used */
	private ProgressDialog mProgressD;
	private Dialog mHelpD;
	protected boolean mShowDialog;
	
	/* Internally the overlays consist of multiple sets. These
	 * variables map keys to the keys
	 */
	private HashMap<String, BusStopOverlay> mOverlayMap;
	private static String sCURRENT_OVERLAYS = "BusOverlays";
	private static String sNEXT_OVERLAYS = "NextOverlays";
	

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.map_search);
    
        mOverlaysAreDisplayed = false;
        mMapLayout = (RelativeLayout)findViewById(R.id.map_layout);
        mListLayout = (LinearLayout)findViewById(R.id.list_layout);
        
        mSearchText = (AutoCompleteTextView) findViewById(R.id.search_text);
        mSearchButton = (ImageButton)findViewById(R.id.search_button);
        mGPSButton = (ImageButton)findViewById(R.id.update_gps_button);
        mListAddress = (TextView)findViewById(R.id.text_list_address);
        mListEmpty = (TextView)findViewById(R.id.list_empty);
        
        mList = (ListView) findViewById(R.id.list);
        View header = getLayoutInflater().inflate(R.layout.list_header, null);
        mList.addHeaderView(header);
        
        mMap= (MapView)findViewById(R.id.map);
        mMap.setBuiltInZoomControls(true);
        mMap.displayZoomControls(true);
        mMap.setClickable(true);
        mMap.setEnabled(true);
      
        mMapOverlays = mMap.getOverlays();
        mMapController = mMap.getController();
        
        mShowDialog = true;
        
        /* Internally keep track of two BusStopOverlays that are displayed on map */
        mOverlayMap = new HashMap<String, BusStopOverlay>();
        mOverlayMap.put(sCURRENT_OVERLAYS, makeBusStopOverlay());
        mOverlayMap.put(sNEXT_OVERLAYS, makeBusStopOverlay());
     
        mLocManager = (LocationManager)getSystemService(Context.LOCATION_SERVICE);
        mLocManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 6000, sLOCATION_BUFFER, this);
      
        startDisplay();
        setListeners();
        setupHelp();
       
        /* Handle if call came from another activity */
        Intent i = getIntent();
        Bundle b = i.getExtras();
        if (b!=null && b.containsKey("isMapView")) {
        	mIsMapView = b.getBoolean("isMapView");
        	populateView();
        }else {
        	mListLayout.setVisibility(View.INVISIBLE);
            mIsMapView = true;
        }
        THREAD_FLAG=false;
        mMap.invalidate();
    }
    
    /** Sets up dynamic aspects of the layout which must be done at this point where the view is 
     * guaranteed to be rendered and contain valid getWidth() and getHeight() values */
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
    	super.onWindowFocusChanged(hasFocus);
    	if(hasFocus){
            LinearLayout top_bar = (LinearLayout)findViewById(R.id.top_bar);
            int search_text_width = top_bar.getWidth()-mSearchButton.getWidth()-mGPSButton.getWidth();
            mSearchText.setWidth(search_text_width);
            mSearchText.setMaxWidth(search_text_width);
    	}
    }
    /** An abstract method inherited that must be implemented */
    @Override
    protected boolean isRouteDisplayed() {
        return false;
    }

    
    /* Begin Menu Area */
    
    /** Creates the menu items */
    public boolean onCreateOptionsMenu(Menu menu) {
    	MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.main_menu, menu);
        MenuItem switch_view = menu.findItem(R.id.menu_item_switch_view);
        switch_view.setIcon(R.drawable.menu_list);
        switch_view.setTitle("Switch View");
        return true;
    }
    
    /** Specifies behavior of menu items 
     *  The menu allows the user to go to Favorites, view Help, or Switch to Map/List view 
     */
    public boolean onOptionsItemSelected(MenuItem item) {
    	Intent i;
	    switch (item.getItemId()) {
			case R.id.menu_item_switch_view:
				switchViews();
				return true;
			case R.id.menu_item_favs:
				i = new Intent(this, Favorites.class);
				startActivity(i);
				return true;
			/*case R.id.menu_item_contacts:
				i = new Intent(this, ContactsView.class);
				startActivity(i);
				return true;*/
			case R.id.menu_item_help:
			  	mHelpD.show();
				return true;
		}
	    return false;
    }

    /** Creates the listeners for the Search/GPS buttons and list*/
    public  void setListeners() {
	      mSearchButton.setOnClickListener(new ImageButton.OnClickListener() {
	            public void onClick(View v) {
	            	if (mSearchText.getText() != null && mSearchText.getText().length() > 0) {
	            		String text = mSearchText.getText().toString().trim();
	            		if (text.length() > 0)
	            			handleUserInput(text);
	            	}
	            }
	        });
	      mGPSButton.setOnClickListener(new ImageButton.OnClickListener() {
	            public void onClick(View v) {
	            	handleGPSUpdate();
	            }
	        });
	      mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
			public void onItemClick(AdapterView<?> parent, View view, int position,
					long rowid) {
		    		toStopView(mCurrentBusStops[position-1].getId());
			}
	      });  
	}
    /**
     * Remove location listeners
     */
	@Override 
	public void onPause() {
		mMyLocation.disableMyLocation();
		mLocManager.removeUpdates(this);
		super.onPause();
	}
    /**
     * Remove location listeners
     */
	@Override
	public void onStop() {
		mMyLocation.disableMyLocation();
		mLocManager.removeUpdates(this);
		super.onStop();
	}
    /**
     * Restart location listeners
     */
	@Override
	public void onResume() {
		mMyLocation.enableMyLocation();
		mLocManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 6000, 10, this);
		super.onResume();
	}
    /**
     * Remove location listeners
     */
	@Override
	public void onDestroy() {
		mMyLocation.disableMyLocation();
		mLocManager.removeUpdates(this);
		super.onDestroy();
	}
	
	/** Override back key so it will go to Map view if in List View */
	@Override
	public boolean onKeyDown (int keyCode, KeyEvent event) {
		if (keyCode==KeyEvent.KEYCODE_BACK && !mIsMapView){
			switchViews();
			return true;
		}
		return super.onKeyDown(keyCode, event);
	}
	
	/** Keep track of the view the user is in. Saves state
	 *  such as map center, zoom, list displays, and overlays. 
	 *
	 */
	@Override
	public void onSaveInstanceState(Bundle savedInstanceState) {
		GeoPoint center = mMap.getMapCenter();
		if (mLastCenterPoint != null) {
			savedInstanceState.putInt("LastLat", mLastCenterPoint.getLatitudeE6());
			savedInstanceState.putInt("LastLon", mLastCenterPoint.getLongitudeE6());
		}
		if (mListAddress.getText() != null && mListAddress.getText().length() > 0)
			savedInstanceState.putString("AddressField", mListAddress.getText().toString());
		savedInstanceState.putInt("MapLat", center.getLatitudeE6());
		savedInstanceState.putInt("MapLon", center.getLongitudeE6());
		savedInstanceState.putBoolean("isMap", mIsMapView);
		savedInstanceState.putInt("Zoom", mMap.getZoomLevel());
		savedInstanceState.putBoolean("OverlaysDisplayed", mOverlaysAreDisplayed);
		if (mCurrentBusStops != null) {
			String [] array = new String[mCurrentBusStops.length];
			for (int i = 0; i < mCurrentBusStops.length; i++) 
				array[i] = mCurrentBusStops[i].getId();
			savedInstanceState.putStringArray("Stops", array);
		}
		super.onSaveInstanceState(savedInstanceState);
	}
	@Override
	public void onRestoreInstanceState(Bundle savedInstanceState) {
		mIsMapView = savedInstanceState.getBoolean("isMap");
		if (savedInstanceState.containsKey("LastLat") && savedInstanceState.containsKey("LastLat"))
			mLastCenterPoint = new GeoPoint (savedInstanceState.getInt("LastLat"), savedInstanceState.getInt("LastLon"));
		if (savedInstanceState.containsKey("AddressField"))
			mListAddress.setText(savedInstanceState.getString("AddressField"));
		int lat = savedInstanceState.getInt("MapLat");
		int lon = savedInstanceState.getInt("MapLon");
		int zoom = savedInstanceState.getInt("Zoom");
		mMapController.setCenter(new GeoPoint (lat, lon));
		mMapController.setZoom(zoom);
		mOverlaysAreDisplayed = savedInstanceState.getBoolean("OverlaysDisplayed");
		if (savedInstanceState.containsKey("Stops")) {
			String [] array = savedInstanceState.getStringArray("Stops");
			mCurrentBusStops = new BusStop[array.length];
			for (int i = 0; i < array.length; i++)
				mCurrentBusStops[i] = BusStop.find(array[i]);
		}
		populateView();
		super.onRestoreInstanceState(savedInstanceState);
	}
	
	/** Override to sense when map center has changed */
	@Override
	public boolean dispatchTouchEvent (MotionEvent event) {
		/* If at max zoom level the map will constantly display bus stops */
		if (event.getAction()==MotionEvent.ACTION_UP && mIsMapView) {
			if (mMap.getZoomLevel() <= 18) 
				return super.dispatchTouchEvent(event);
			if (mLastQueryCenter==null) {
				mLastTouchCenter = mMap.getMapCenter();
				mLastQueryCenter = mLastTouchCenter;
				mListAddress.setText("Map Center");
				findThread(mLastQueryCenter, false, false, sCURRENT_OVERLAYS);
				return super.dispatchTouchEvent(event);
			}
			GeoPoint current=mMap.getMapCenter();
			
			/* Compute a new query if the map center has changed sufficiently
			 * Default Query radius - sLOCATION-BUFFER - radius of screen = distance between center */
			double distance = FirbiHelper.getDistance(current, mLastQueryCenter, true);
			if (distance<= sDEFAULT_RADIUS-sLOCATION_BUFFER-125 || FirbiHelper.getDistance(current, mLastTouchCenter, true) < 5) {
				mLastTouchCenter = current;
				return super.dispatchTouchEvent(event);
			}
			GeoPoint newQueryCenter = computeNewQueryCenter(mLastTouchCenter, current, mLastQueryCenter);
			if (newQueryCenter != null) {
				
				/* Display next overlays then remove old ones */
				findThread(newQueryCenter, false, false, sNEXT_OVERLAYS);
				BusStopOverlay currentOverlays = mOverlayMap.get(sCURRENT_OVERLAYS);
				removeOverlay(currentOverlays);
				mOverlayMap.put(sCURRENT_OVERLAYS, mOverlayMap.get(sNEXT_OVERLAYS));
				mOverlayMap.put(sNEXT_OVERLAYS, currentOverlays);
				mLastQueryCenter = newQueryCenter;
				mLastCenterPoint = mLastQueryCenter;
				mListAddress.setText("Map Center");
				mMap.invalidate();
			}
			mLastTouchCenter = current;
		}
		return super.dispatchTouchEvent(event);
	}
	/**
	 * Computes the new query center by projecting the GeoPoints to XYZ, computing a new XYZ point
	 * based on distance, then computing the final GeoPoint from XYZ.
	 * @param oldTouch: Previous map center
	 * @param currentTouch: Current center of the map
	 * @param oldQuery: Old query point
	 * @return: GeoPoint of new query center, or null if there should not be a new query
	 */
	private GeoPoint computeNewQueryCenter(GeoPoint oldTouch, GeoPoint currentTouch, GeoPoint oldQuery) {  
		/* For following conversion formulas are the Geodetic Datum formulas for ECEF XYX and lat lon.
		 * Formula is not completely accurate -- assumes Earth is a sphere
		 * and the Z coordinate will not change at this zoom level. 
		 */
		double theta = (oldTouch.getLatitudeE6()/1E6)/57.2958;
		double phi = oldTouch.getLongitudeE6()/1E6/57.2958;
		double r = 6378.1*1000;
		double oldX = r*Math.sin(theta)*Math.cos(phi);
		double oldY = r*Math.sin(theta)*Math.sin(phi);
		double oldZ = r * Math.cos(theta);
		
		theta = (currentTouch.getLatitudeE6()/1E6)/57.2958;
		phi = (currentTouch.getLongitudeE6())/1E6/57.2958;
		
		/* Once in XYZ, uses geometry, slope, and the pythagorean theorum 
		 * to project a new query point in direction user is moving. 
		 */
		double currentX = r*Math.sin(theta)*Math.cos(phi);
		double currentY = r*Math.sin(theta)*Math.sin(phi);
		double slope = Math.abs((currentY-oldY)/(currentX-oldX));
		
		double newLat = Math.sqrt(Math.pow((sDEFAULT_RADIUS), 2)/(1+Math.pow(slope, 2)));
		double newLon = Math.abs(slope*newLat);
		newLon = (oldTouch.getLongitudeE6()<currentTouch.getLongitudeE6()) ? (-1*newLon) : newLon;
		newLat = (oldTouch.getLatitudeE6()<currentTouch.getLatitudeE6()) ? (-1*newLat) : newLat;
		newLon = newLon + currentY;
		newLat = newLat + currentX;
		/* Convert XYZ back to lat lon */
		double lonCoord = Math.atan2(newLon, newLat) * 57.2958;
		double p = Math.sqrt(Math.pow(newLon, 2)+ Math.pow(newLat, 2));
		double latCoord = Math.atan2(p, oldZ) * 57.2958;
		
		GeoPoint newCenter = new GeoPoint((int)(latCoord*1E6), (int)(lonCoord*1E6));
		float [] result = new float[1];
		Location.distanceBetween(oldQuery.getLatitudeE6()/1E6, oldQuery.getLongitudeE6()/1E6,
				newCenter.getLatitudeE6()/1E6, newCenter.getLongitudeE6()/1E6, result);

		/* If the new query point is sufficiently a part from the previous point
		 * then use it, else return null. 
		 */
		if (result[0] < sDEFAULT_RADIUS*1.5-sLOCATION_BUFFER)
			return null;
		return newCenter;
	}
	
	
	//---------------------------------------------------
	// Functional methods
	
	/**
	 * Populates the Map or list with the current bus stops
	 * and sets the view (Map or List) depending on the state of
	 * the activity. 
	 */
	protected void populateView() {
		if (mIsMapView) {
			mMapLayout.setVisibility(View.VISIBLE);
			mListLayout.setVisibility(View.INVISIBLE);
			mListEmpty.setVisibility(View.INVISIBLE);
		}else {
			mMapLayout.setVisibility(View.INVISIBLE);
			mListLayout.setVisibility(View.VISIBLE);
			boolean temp = mOverlaysAreDisplayed;
			removeAllOverlays();
			mOverlaysAreDisplayed = temp;
		}
		if (!mIsMapView || (mIsMapView && mOverlaysAreDisplayed))
			refreshBusStops(mLastCenterPoint, mCurrentBusStops, mOverlayMap.get(sCURRENT_OVERLAYS));
	}
	
	/** Switch between map/list view */
	public  void switchViews() {
		mIsMapView = !mIsMapView;
		populateView();
	}


    /** Displays location and zooms in to Seattle */
	public  void startDisplay() {
		mMyLocation = new MyLocationOverlay(this, mMap);
		mMyLocation.enableMyLocation();
		mMapOverlays.add(mMyLocation);
		
		mLastCenterPoint = Seattle;
		mLastLocation = null;
		zoomToLocation(Seattle, 13);
		mValidLocation = false;
	}

	/** Displays bus stop on the map or list 
	 * 
	 * @param loc: Location center point to search for bus stops
	 */
	public synchronized void  refreshBusStops(GeoPoint loc, BusStop [] stops) {
		refreshBusStops(loc, stops, mOverlayMap.get(sCURRENT_OVERLAYS));
	}
	
	/**
	 * Displays the bus stop set in map or list. Internally there are more than
	 * one BusStopOverlay sets being displays, this method allows for a specific
	 * set to be displayed. 
	 * @param loc: Center point location of the bus stops
	 * @param stops: Array of stops to be displayed
	 * @param overlays: The set of overlays to display the stops
	 */
	private synchronized void refreshBusStops(GeoPoint loc, BusStop [] stops, BusStopOverlay overlays) {
		if (overlays==null) 
			return; 
		mOverlaysAreDisplayed = true;
		if (mIsMapView) {
			displayBus(stops, overlays);
		}else if (!mIsMapView && loc!=null){
			listBus(stops, loc);
		}
	}
	
	
	/** Lists the bus stops (List view)
	 * 
	 * @param stops: Bus stops to list
	 * @param loc: Location center of the bus stops
	 */
	protected  void listBus(BusStop [] stops, GeoPoint loc) {
			if (stops==null || stops.length==0) {
				mCurrentBusStops = null;
				mList.setAdapter(null);
				manageListView(INPUT_NOTHING);
				return;
			}else
				manageListView(INPUT_BUS_STOP);
			
			double distance;
			Arrays.sort(stops, new BusStopComparator(loc));
			ArrayList<HashMap<String,String>> list = new ArrayList<HashMap<String,String>>();
			for (int i = 0; i < stops.length; i++) { 
				distance = FirbiHelper.getDistance(loc, stops[i].getLocation(), false);
				HashMap<String,String> item = new HashMap<String,String>();
				item.put( "dir", ""+FirbiHelper.directionToString(stops[i].getDirection()));
				item.put( "add", stops[i].getAddress());
				item.put( "dist", ""+distance);
				list.add(item);
			}
			mList.setAdapter(new SimpleAdapter(this, list, R.layout.list_entry,
				new String[] { "dir","add", "dist"}, 
				new int[] { R.id.direction, R.id.address, R.id.distance}));
	}
	
	/** Displays bus stop overlays (Map view) to the user.
	 * 
	 * @param stops: Bus stops to be displayed
	 */

	protected synchronized void displayBus(BusStop[] stops) {
		displayBus(stops, mOverlayMap.get(sCURRENT_OVERLAYS));
	}
	
	/**
	 * Used to display stop overlays (Map view). Adds the overlays
	 * to the provided set of overlays because internally there is more than one
	 * overlay set. 
	 * @param stops: BusStops to be displayed
	 * @param overlays: Set of overlays to add the stops to
	 */
	private synchronized void displayBus(BusStop[] stops, BusStopOverlay overlays) {  
		if (stops==null || stops.length==0) {
			displayMessage(null, NO_BUS_STOP);
			return;
		}else {
			BusStopOverlayItem item;
			for (int i = 0; i < stops.length; i++) {
				item = new BusStopOverlayItem(stops[i].getLocation(), stops[i]);
				overlays.addOverlay(item);
			}
			mMapOverlays.add(overlays);
			mOverlaysAreDisplayed = true;
		}
	}
	
	
	/** Removes all bus and contact overlays. 
	 *  The MyLocation Overlay will not be removed. */
	public synchronized void removeAllOverlays() {
		mOverlaysAreDisplayed = false;
		Set<String> keys =  mOverlayMap.keySet();
		Iterator<String> it = keys.iterator();
		while (it.hasNext()) {
			BusStopOverlay current= mOverlayMap.get(it.next());
			removeOverlay(current);
		}
	}
	
	/**
	 * Removes the BusStopOverlay set from the map.
	 * @param item: Set of overlays to remove
	 */
	private synchronized void removeOverlay(BusStopOverlay item) {
		if (item != null && mMapOverlays.contains(item)) {
			mMapOverlays.remove(item);
			item.removeAll();
		}
	}

    /** 
     * Displays a message for the user. Depending on the type code it will
     * display a short error message or a message in response to the user's
     * query.
     * 
     * @param input: Text user entered
     * @param type: Type of message to be displayed
     */
    public void displayMessage(String input, int type) {
    	String msg = "";
    	switch (type) {
    		case INPUT_ADDRESS: msg = "Unable to find Address '" + input + "'"; break;
    		case INPUT_BAD_BUS_STOP: msg = "Unable to find Bus Stop '" + input + "'"; break;
    		case NO_BUS_STOP: msg = "No Bus Stops Found"; break;
    		case INPUT_BAD_ROUTE: msg = "Unable to find Bus Route '" + input + "'"; break;
    		case NO_GPS: msg = "No GPS Signal"; break;
    		default: msg = "Unable to find '" + input + "'";
    	}
    	Toast t = Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT);
    	t.setGravity(Gravity.TOP, 0, 70);
    	t.show();
    }

    /**
     * This method displays a help dialog for the user describing 
     * Search, its functions, and how to use it.
     */
    public void setupHelp() {
    	LayoutInflater li = (LayoutInflater)getApplicationContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    	View v = li.inflate(R.layout.search_help_dialog, null);
        RelativeLayout help = (RelativeLayout)v.findViewById(R.id.search_help);
        
        LinearLayout top_bar = (LinearLayout)findViewById(R.id.top_bar);
        help.setMinimumWidth(top_bar.getWidth()-mSearchButton.getWidth());
        help.setMinimumHeight(mMap.getHeight()-top_bar.getHeight());
        
        TabHost helpTab = (TabHost)v.findViewById(R.id.tabhost);
        helpTab.setup();
        TabSpec ts = helpTab.newTabSpec("TAB_TAG_1"); 
        ts.setIndicator("Features");
        ts.setContent(R.id.search_help_tab_main);
        helpTab.addTab(ts);
        
        TabSpec ts2 = helpTab.newTabSpec("TAB_TAG_2"); 
        ts2.setIndicator("Icons");
        ts2.setContent(R.id.search_help_tab_icon);
        helpTab.addTab(ts2);
        
        TabSpec ts3 = helpTab.newTabSpec("TAB_TAG_3"); 
        ts3.setIndicator("Menu");
        ts3.setContent(R.id.search_help_tab_menu);
        helpTab.addTab(ts3);
        
        mHelpD = new Dialog(this);
        Button b = (Button)v.findViewById(R.id.btnExit);
        b.setOnClickListener(new OnClickListener() {
			public void onClick(View v) {
				mHelpD.dismiss();
			}
        });
    	mHelpD.setTitle("Search Help");
    	mHelpD.setContentView(v);
    	mHelpD.setCanceledOnTouchOutside(true);
    }
   
    /** 
     * Handles user input by either display the correct bus stops, sending the user
     * to the next view, or displaying an error message if nothing can be found. 
     * @param s: User input string
     */
    protected void handleUserInput(String s) {
    	int type = checkInputType(s);
    	switch(type) {
    		case INPUT_ROUTE: 
    			handleRouteInput(s);
    			break;
    		case INPUT_ADDRESS: 
    			handleAddressInput(s);
    			break;
    		case INPUT_BUS_STOP: 
    			handleStopInput(s);
    			break;
    		case INPUT_BAD_BUS_STOP: displayMessage(s, INPUT_BAD_BUS_STOP); break;
    		case INPUT_BAD_ROUTE: displayMessage(s, INPUT_BAD_ROUTE); break;
    		default: displayMessage(s, -1);
    	}
    }
    
    /** Handles the GPS button click event.
     *  Will find bus stops near the user's GPS location if it is accurate enough
     *  or will use the map center point if there is no GPS signal. 
     */
    protected void handleGPSUpdate() {
    	Location loc = mLocManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
    	boolean validLoc = validLocation(loc, System.currentTimeMillis());
    	boolean sameLoc = validLoc && FirbiHelper.sameLocation(FirbiHelper.locToGeo(loc), mLastLocation);
     	if (validLoc)
     		Log.v("GPS Stats", ""+loc.getTime() + "\t" + loc.getAccuracy());
    	
    	/* List View -- Only update if you have GPS else display 
    	 * an message indicating no GPS */
    	if (!mIsMapView) {
        	if  (!validLoc) {
        		manageListView(NO_GPS);
        		return;
        	}else if (sameLoc)
        		return;
        	else 
        		manageListView(INPUT_BUS_STOP);
    	}
    	GeoPoint centerPoint;
    	
    	if (!validLoc) {      /* Just use center of map */
    		centerPoint = mMap.getMapCenter();
    		mListAddress.setText("Map Center");
    	}else{                 /* Get and display new location */
    		centerPoint = new GeoPoint((int)(loc.getLatitude()*1E6), (int)(loc.getLongitude()*1E6));
    		mLastLocation = centerPoint;
    		mMyLocation.enableMyLocation();
    		zoomToLocation(mLastLocation, 0);
    		mListAddress.setText("My Location");
    	}
    	sameLoc = sameLoc || FirbiHelper.sameLocation(centerPoint, mLastCenterPoint);
    	mLastCenterPoint = centerPoint;
    	
    	/* Same point on map -- toggle between displaying overlays and removing them*/
    	if (sameLoc && mCurrentBusStops != null) {
    		if (mIsMapView && mOverlaysAreDisplayed) {
    			removeAllOverlays();
    		} else if (mIsMapView && !mOverlaysAreDisplayed) {
    			refreshBusStops (centerPoint, mCurrentBusStops, mOverlayMap.get(sCURRENT_OVERLAYS));
    		}
    	}else {        /* Get new stops */
    		if (mIsMapView)
    			removeAllOverlays();
    		findThread(mLastCenterPoint, true, true, sCURRENT_OVERLAYS);
    	}
		mMap.invalidate();
    }

    
    /* =================================================================== */
    /*  Random helper methods */
    
    
    /** Checks the user input string to see if it can be
     *  either a bus route, stop, or address.
     *  Will also accept the keywords 'route' and 'stop'.
     *  @param s: User input. Must be checked to be not null and of at least length one. 
     *  @return: Classification code of input
     */
    protected int checkInputType(String s) {
    	int len = s.length();
    	s = s.toLowerCase();

    	if (len <= 3) 
    		return s.matches("^[0-9]+") ? INPUT_ROUTE : INPUT_BAD_ROUTE;
    	else if (len <=6 && s.matches("^[0-9]*"))
    		return INPUT_BUS_STOP;
    	else if (s.matches("route ?[0-9]+"))
    		 return INPUT_ROUTE;
    	else if (s.matches("stop ?[0-9]+"))
    		return INPUT_BUS_STOP;
    	else
    		return INPUT_ADDRESS;
    }
    

    /**
     * Handles when the user enters a route number. Will either display an error message
     * or send the user to another page to continue the query. 
     * @param s: Original user input string
     */
    protected void handleRouteInput(String s) {
    	GeoPoint center;
    	Location loc = mLocManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
    	if (!mIsMapView && validLocation(loc, System.currentTimeMillis()))
    		center = FirbiHelper.locToGeo(loc);
    	else
    		center = mMap.getMapCenter();
    	routeThread(s, center);
    }
    
    /**
     * Handles when the user enters an address. Will either display an error message if
     * no address or no bus stops are found or display the bus stops near that location.
     * @param s: Original use input string
     */
    protected void handleAddressInput(String s) {
    	addressThread(s);
    }
    
    /**
     * Handles when the user enters a stop number. Will either display an error message
     * if the stop number cannot found or will take the use to another page
     * that displays all the arrivals for that stop.
     * @param s: Original input string
     */
    protected void handleStopInput(String s) {
    	stopNumberThread(s, mMap.getMapCenter());
    }
    
    /** Prompts the user to select a more specific route
     * 	when two or more were returned in the original query to OBA.
     *  pre: str must be null and length > 1
     */
    protected void selectRoute (String [] str, String input, final GeoPoint center) {
    	ArrayList<String> routeDisplay = new ArrayList<String>();
    	final BusRoute[] routes = new BusRoute[str.length];
    	for (int i = 0; i < str.length; i++) {
    		routes[i] = BusRoute.find(str[i]);
    		routeDisplay.add(routes[i].getRouteNumber() + "\t" + routes[i].getAgencyId());	
    	}
    	ArrayAdapter<String> listAdapt = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, routeDisplay);
	   	LayoutInflater li = (LayoutInflater)getApplicationContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    	View v = li.inflate(R.layout.alertdialog_list, null);
    	ListView list = (ListView)v.findViewById(R.id.adList);
		list.setAdapter(listAdapt);
		
	    final Dialog d = new Dialog(this);
	    d.setTitle("Choose a Route '" + input + "'");
	    d.setContentView(v);
	    d.setCanceledOnTouchOutside(true);
	    list.setOnItemClickListener(new OnItemClickListener () {
			public void onItemClick(AdapterView<?> arg0, View arg1, int position,
					long arg3) {	
				d.dismiss();
				toRouteView(routes[position].getId(), center);
			}
	    });
	    d.show();
    }
    
    /** Prompts the user to select a more specific stop
     * 	when two or more were returned in the original query to OBA.
     */
    protected void selectStop (String[] str, String input) {
    	ArrayList<String> stopDisplay = new ArrayList<String>();
    	final BusStop [] stops = new BusStop[str.length];
    	for (int i = 0; i < str.length; i++) {
    		stops[i] = BusStop.find(str[i]);
    		stopDisplay.add(stops[i].getAgencyId() + "\t" + stops[i].getAddress());
    	}
    	LayoutInflater li = (LayoutInflater)getApplicationContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    	View v = li.inflate(R.layout.alertdialog_list, null);
    	ListView list = (ListView)v.findViewById(R.id.adList);
    	list.setAdapter(new ArrayAdapter<String> (this, android.R.layout.simple_list_item_1, stopDisplay));
    	final Dialog d = new Dialog(this);
    	d.setTitle("Choose a Stop '" + input + "'");
    	d.setContentView(v);
    	d.setCanceledOnTouchOutside(true);
    	list.setOnItemClickListener(new OnItemClickListener () {
			public void onItemClick(AdapterView<?> arg0, View arg1, int position,
					long arg3) {
				d.dismiss();
				toStopView(stops[position].getId());
			}
    	});
    	d.show();
    }
    
    
    /** 
     * Sets the different views with different text displays in list view.
     * Toggles the list between normal display, empty display, and no GPS
     * @param code: Display code for which display
     */
    private void manageListView (int code) {
    	switch (code) {
    	case INPUT_BUS_STOP: 
    			mListLayout.setVisibility(View.VISIBLE);      /* Normal */
    			mListEmpty.setVisibility(View.INVISIBLE);
    			break;
    	case NO_GPS: 
    			displayMessage(null, NO_GPS);                  /* No GPS */
				break;
    	case INPUT_NOTHING: 
    			mListLayout.setVisibility(View.INVISIBLE);      /* empty */
				mListEmpty.setVisibility(View.VISIBLE);
				mListEmpty.setText("No Bus Stops Found");
				break;
    	}
    }

    /* Intent Methods */
    
    /** Fires an intent to stop view and passes a stop's id
     *  pre: id must be validated to be an actual bus stop id
     * @param id: Bus stop id
     */
    protected void toStopView (String id) {
    	Intent i = new Intent(this, StopView.class);
    	i.putExtra(sSTOP_KEY, id);
    	startActivity(i);
    }
    
    /**
     * Fires an intent to route view and passes a route's id
     * pre: route must be validated to be an actual bud route id
     * @param route: Bus route id
     */
    protected void toRouteView (String route, GeoPoint loc) {
    	Intent i = new Intent(this, RouteView.class);
    	i.putExtra(sROUTE_KEY, route);
    	i.putExtra(sROUTE_LAT_KEY, loc.getLatitudeE6());
    	i.putExtra(sROUTE_LON_KEY, loc.getLongitudeE6());
    	startActivity(i);
    }
    
    /**
     * Check whether the location is valid based on time and accuracy of fix
     * @return: True is location is valid, false otherwise
     */
    protected boolean validLocation(Location loc, long currentTime) {
    	if (loc==null || loc.getAccuracy() > sMIN_GPS_ACC)
    		return false;
    	if (Math.abs(loc.getTime()-currentTime) <= sMIN_GPS_TIME) { /* Sometimes the time stamps work and sometimes they don't */
    		mValidLocation = true; 
    	}
    	return mValidLocation;            /* mValidLocation is another check in case the time stamp is not working */
    }

 
    /**
     * Creates a new BusStopOverlay and sets it to display
     * a bus stop. Sets the overlay's map to this MapView.
     * @return: BusStopOverlay with a bus icon
     */
    private BusStopOverlay makeBusStopOverlay() {
    	Drawable drawable = this.getResources().getDrawable(R.drawable.bus);
    	BusStopOverlay newOverlay = new BusStopOverlay(drawable);
    	newOverlay.setMap(mMap);
    	return newOverlay;
    }
    
	
	/**
	 *  Zooms map into a location. If level is 0 will
     *  zoom to the default level (17).
     * @param p: Center point
     * @param level: Zoom level
     */
    private void zoomToLocation(GeoPoint p, int level) {
    	if (level==0)
    		level=17;
    	mMapController.animateTo(p);
    	mMapController.setZoom(level);
    }
    

    
    
    
    /** Returns the current bus stops */
    public BusStop[] getCurrentBusStops() {
    	if (mCurrentBusStops==null)
    		return null;
    	return mCurrentBusStops.clone();
    }
    /** Returns current location */
    public GeoPoint getMyLocation() {
    	return mLastLocation;                 
    }
    /** Returns if in map or list view */
    public boolean isMapView() {
    	return mIsMapView;
    }
    /**
     * Turns on the process dialog for this activity. The progress dialog
     * is on by default. 
     */
    public void turnOnProgressDialog() {
    	mShowDialog = true;
    }
    /**
     * Turns off the process dialog for this activity. The process dialog
     * is on by default.
     */
    public void turnOffProgressDialog() {
    	mShowDialog = false;
    }
    /**
     * Returns a flag that signals whether the activity has finished processing the user input.
     * @return
     */
    public boolean getFlag() {
    	return THREAD_FLAG;
    }
 
	/*=====================================================
	 * Threads to handle requests
	 * 
	 */
	
	/** Creates a separate thread to execute BusStop.find
	 * @param point: Center of find request
	 * @param zoom: Whether or not to zoom if find was successful
	 * @param display: Whether or not to display a progress dialog
	 * @param overlays: Which overlay set to add the resulting bus stops to
	 */
	private void findThread(final GeoPoint point, final boolean zoom, final boolean display, final String overlays) {
		THREAD_FLAG=false;
		if (mShowDialog && display) 
			mProgressD = ProgressDialog.show(mMap.getContext(), "Searching...", "Finding bus stops", true,
					false);
		Thread find = new Thread() {
			public void run() {
				mCurrentBusStops = BusStop.find(point, sDEFAULT_RADIUS);
				Message msg = new Message();
				Bundle b = new Bundle();
				b.putInt("Lat", point.getLatitudeE6());
				b.putInt("Lon", point.getLongitudeE6());
				b.putString("Overlays", overlays);
				b.putBoolean("Zoom", zoom);
				b.putBoolean("Display", display);
				msg.setData(b);
				findHandler.sendMessage(msg);
			}
		};
		find.start();
	}
	/** Populates the UI with the new bus stops from BusStop.find
	 */
	private Handler findHandler = new Handler() {
		@Override
		public void handleMessage(Message msg) {
			THREAD_FLAG=true;
			Bundle b = msg.getData();
			Boolean displayOn = b.getBoolean("Display");
			if (mShowDialog && displayOn)
				mProgressD.dismiss();
			boolean zoom = b.getBoolean("Zoom");
			GeoPoint p = new GeoPoint(b.getInt("Lat"), b.getInt("Lon"));
			if ( !displayOn && (mCurrentBusStops==null || mCurrentBusStops.length==0) )
				return;
			mOverlayMap.put(b.getString("Overlays"), makeBusStopOverlay());
			refreshBusStops(p, mCurrentBusStops, mOverlayMap.get(b.getString("Overlays")));
			if (zoom && mCurrentBusStops != null && mCurrentBusStops.length > 0)
				zoomToLocation(p, 0);
			mMap.invalidate();
		}
	};
	
    /**
     * Takes an address in string form and finds the bus stops near that address.
     * Will only search addresses in WA. 
     * @param s: Address string to turn into a GeoPoint
     * @return: GeoPoint of address or null
     */
    private void addressThread(final String s) {
    	THREAD_FLAG=false;
    	if (mShowDialog) 
			mProgressD = ProgressDialog.show(mMap.getContext(), "Searching...", "Finding bus stops", true,
					false);
	
		/* Add WA to address if it's not already there */
    	final String input= (!s.toLowerCase().matches(".*[ ,](wa|washington)($|[ ,].*)"))? 
    			s.concat(", WA"): s;
    
        final Geocoder geocoder = new Geocoder(this, Locale.US);
        Thread t = new Thread() {
	        public void run() {
	        	Message msg = new Message();
	        	Bundle b = new Bundle();
	        	try {
		          List<Address> addressResult = geocoder.getFromLocationName(input, 1);
		          if (!addressResult.isEmpty()) {
		            Address resultAddress = addressResult.get(0);
		            String state = resultAddress.getAdminArea();
		            if (state != null && state.toLowerCase().matches("wa|washington")) {
		            	GeoPoint p = new GeoPoint((int)(resultAddress.getLatitude()*1E6), (int)(resultAddress.getLongitude()*1E6));
		            	mCurrentBusStops= BusStop.find(p, sDEFAULT_RADIUS);
		            	b.putInt("Lat", (int)(resultAddress.getLatitude()*1E6));
			            b.putInt("Lon", (int)(resultAddress.getLongitude()*1E6));
			            b.putString("Input", s);
			            b.putBoolean("Success", true);
			            msg.setData(b);
			            addressHandler.sendMessage(msg);
			            return;
		            }
		           }
		        }catch (Exception e) {  /* Error with lookup */
		        }
	        	b.putString("Input", s);
	        	b.putBoolean("Success", false);
	        	msg.setData(b);
	        	addressHandler.sendMessage(msg);
	        }
	    };
	    t.start();
    }
    /**
     * Handler for addressThread. Updates the bus stops found (if any)
     * or displays a message if none are found. 
     */
    private Handler addressHandler = new Handler() {
    	@Override
    	public void handleMessage (Message msg) {
    		THREAD_FLAG=true;
    		if (mShowDialog)
    			mProgressD.dismiss();
    		Bundle b = msg.getData();
    		if (b.containsKey("Success") && b.getBoolean("Success")) {
    			if (!b.containsKey("Lat") || !b.containsKey("Lon") || !b.containsKey("Input"))
    				return;
    			GeoPoint point = new GeoPoint(b.getInt("Lat"), b.getInt("Lon"));
    			mLastCenterPoint = point;
    			if (mCurrentBusStops != null && mCurrentBusStops.length > 0) {
    				removeAllOverlays();
    				mOverlayMap.put(sCURRENT_OVERLAYS, makeBusStopOverlay());
    				refreshBusStops(point, mCurrentBusStops, mOverlayMap.get(sCURRENT_OVERLAYS));
    				zoomToLocation(point, 0);
    				mListAddress.setText(b.getString("Input"));
    				mMap.invalidate();
    			}else {
    				zoomToLocation(point, 0);
    				displayMessage(null, NO_BUS_STOP);
    			}
    		}else if (b.containsKey("Input"))
    			displayMessage(b.getString("Input"), INPUT_ADDRESS);
    	}
    };
    
    /**
     * Thread to handle searching for a bus stop by it's number.
     * @param busStopId: User input stop number
     * @param point: Center location of the query
     */
    private void stopNumberThread(final String busStopId, final GeoPoint point) {
    	final String busStopIdNumber = busStopId.replaceAll("[a-zA-Z ]", "");
    	THREAD_FLAG=false;
    	if (mShowDialog) 
    		mProgressD = ProgressDialog.show(mMap.getContext(), "Searching...", "Finding bus stop", true,
					false);
    	Thread t = new Thread() {
    		public void run() {
    			Message msg = new Message();
    			Bundle bundle = new Bundle();
    			BusStop [] b = BusStop.findByNumber(busStopIdNumber, point);
    			if (b==null || b.length==0) 
    				bundle.putStringArray("Stops", null);
    			else {
    				String [] array = new String[b.length];
    				for (int i = 0; i < b.length; i++) 
    					array[i] = b[i].getId();
    				bundle.putStringArray("Stops", array);
    			}
    			bundle.putString("Input", busStopId);
    			msg.setData(bundle);
    			stopNumberHandler.sendMessage(msg);
    		}
    	};
    	t.start();
    }
    
    /**
     * Handler for findNumberThread. Either displays an error message if
     * no stops are found, prompts the user to select a more specific stop,
     * or directs the user to StopView. 
     */
    private Handler stopNumberHandler = new Handler() {
    	@Override
    	public void handleMessage(Message msg) {
    		THREAD_FLAG=true;
    		if (mShowDialog)
    			mProgressD.dismiss();
			Bundle b = msg.getData();
			String [] array = b.getStringArray("Stops");
			if (array==null)
    			displayMessage(""+b.getString("Input"), INPUT_BAD_BUS_STOP);
    		else if (array.length >= 2)
    			selectStop(array, b.getString("Input"));
    		else  
    			toStopView(array[0]);
        }
    };
    
    /**
     * Thread to handle user input for route search. 
     * @param routeNum: Route number 
     * @param center: Center point of query
     */
    private void routeThread(final String routeNum, final GeoPoint center) {
    	final String routeNumNumeric = routeNum.replaceAll("[a-zA-Z ]", "");
    	THREAD_FLAG=false;
    	if (mShowDialog) 
    		mProgressD = ProgressDialog.show(mMap.getContext(), "Searching...", "Finding bus route", true,
					false);
    	Thread t = new Thread() {
    		public void run() {
    			Message msg = new Message();
    			Bundle b= new Bundle();
    			BusRoute [] routes = BusRoute.find(routeNumNumeric, center);
    			if (routes==null || routes.length==0)
    				b.putStringArray("Routes", null);
    			else {
    				String [] array = new String [routes.length];
    				for (int i = 0; i < routes.length; i++) 
    					array[i] = routes[i].getId();
    				b.putStringArray("Routes", array);
    				b.putInt("Lat", center.getLatitudeE6());
    				b.putInt("Lon", center.getLongitudeE6());
    			}
    			b.putString("Input", routeNum);
    			msg.setData(b);
    			routeHandler.sendMessage(msg);	
    		}
    	};
    	t.start();
    }
    
    /**
     * Handles route threads. Either displays an error message if no
     * routes are found, prompts the user for a more specific route, or directs
     * the user to the next view. 
     */
    private Handler routeHandler = new Handler() {
    	@Override
    	public void handleMessage(Message msg) {
    		THREAD_FLAG=true;
    		if (mShowDialog)
    			mProgressD.dismiss();
    		Bundle b = msg.getData();
    		String [] array = b.getStringArray("Routes");
    		if (array==null)
    			displayMessage(b.getString("Input"), INPUT_BAD_ROUTE);
    		else if (array.length >=2 )
    			selectRoute(array, b.getString("Input"), new GeoPoint(b.getInt("Lat"), b.getInt("Lon")));
    		else
    			toRouteView(array[0], new GeoPoint(b.getInt("Lat"), b.getInt("Lon")));
    	}
    };
    
	/* Location listener methods -- must be implemented*/
	public void onLocationChanged(Location arg0) {
		mValidLocation = true;
	}
	public void onProviderDisabled(String provider) {
		mValidLocation = false;
	}
	public void onProviderEnabled(String provider) {
		mValidLocation = true;
		Location loc = mLocManager.getLastKnownLocation(provider);
		mLastLocation = new GeoPoint((int)(loc.getLatitude()*1E6), (int)(loc.getLongitude()*1E6));
	}
	public void onStatusChanged(String provider, int status, Bundle extras) {	
	}

}
